框架调研
由于公司核心服务均在阿里生态,故以适配阿里云百炼为基准做框架调研
核心业务是通用推理+视觉理解推理
dashscope-sdk-java
- 阿里百炼SDK
- 这个sdk更倾向于服务于处理非通用AI,比如AI语音生成,图像成生成这些
- 官方文档里通用文字推理及视觉理解类的java示例均使用OpenAI SDK
- 由于官方文档示例比较少,对于深度思考/流式输出/多模态 等都没有比较完整的示例
OpenAI SDK
- OpenAI 官方SDK,阿里大部分通用模型支持走baseUrl使用
- 默认使用okhttp做客户端
- 由于原代码是kt写的,构建消息及模态消息需要套几层build
- 没有ChatMemory相关框架及抽象,需要自行实现
Spring AI Alibaba
- 个人理解为阿里ai的Spring AI 封装,内核还是dashscope
- 官网: https://java2ai.com
- Spring AI中文文档: https://docs.springframework.org.cn/spring-ai
- 测试文本推理没有大问题,但视觉理解按官方文档调用报错
- 并且不知道是不是版本原因,官方文档很多示例无法运行,发布了1.0.0.3版本,但没有对应文档
LangChain4j
- 推荐
- LangChain框架的Java版本
- 由于原框架是python所以java版本的api也比较简洁
- 比如写个ImageContent在大部分框架都需要套几层build或套几层方法,在LangChain里只需要
userMsgBuilder.addContent(ImageContent.from(url));
- 有ChatMemory相关抽象,可实现ChatMemoryStore,ChatMemoryProvider来做基于窗口或tokens的db/redis持久化记忆
- 提供高层次的AiServices封装,并且对Message模板做了注解抽象,但不支持多模态
- 提供了PromptTemplate封装,用于处理带参的Prompt工程
- 使用part流的方式提供流式输出,相对OutputStream更实用
LangChain4j 使用
引入
- 没有编排等高级需求的话仅需要引入主包
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
<version>1.5.0</version>
</dependency>
Chat
- 入参
public class AiChatRequest { @Schema(description = "聊天会话id") private String sessionId; @Schema(description = "图片url列表") private List<String> imgUrls; @Schema(description = "文本内容") private String text; }
- 实始化及不带聊天记忆的chat
@PostConstruct private void init(){ this.chatModel = OpenAiChatModel.builder() .baseUrl(baseUrl).apiKey(apiKey) .modelName(defaultModelName) .temperature(0.6) //AI做题建议用低温度 .logRequests(logRequests) .returnThinking(false) .timeout(Duration.ofSeconds(20)) .build(); } //不带聊天记忆的chat public String chat(AiChatRequest request){ var userMsg = genUserMsg(request.getImgUrls(),request.getText()); var useVlModel = CollectionUtil.isNotEmpty(request.getImgUrls()); var chatRequest = genChatRequest(userMsg,useVlModel); var chatResp = chatModel.chat(chatRequest); var aiMsg = chatResp.aiMessage(); return aiMsg.text(); } //生成userMessage private UserMessage genUserMsg(List<String> imageUrls, String text){ if(CollectionUtil.isEmpty(imageUrls)){ return UserMessage.from(text); }else { var msgBuilder = UserMessage.builder(); imageUrls.forEach(url -> { if(StringUtils.isNotBlank(url)) msgBuilder.addContent(ImageContent.from(url)); }); if(StringUtils.isNotBlank(text)) msgBuilder.addContent(TextContent.from(text)); return msgBuilder.build(); } } //生成chatRequest private ChatRequest genChatRequest(UserMessage userMessage,Boolean useVlModel){ var requestBuilder = ChatRequest.builder(); //关闭思考 var parameters = OpenAiChatRequestParameters.builder().customParameters(Map.of("enable_thinking", false)).build(); //带图片的消息使用视觉理解模型 if(useVlModel) return requestBuilder.modelName(vlModelName).parameters(parameters).messages(userMessage).build(); return requestBuilder.messages(userMessage).parameters(parameters).build(); }
ChatMemory
- 创建基于redis的memoryStore
static class RedisPersistentChatMemoryStore implements ChatMemoryStore { private StringRedisTemplate redisTemplate; private int expireSeconds; private String keyPrefix = "chat_memory:"; private RedisPersistentChatMemoryStore(){} public RedisPersistentChatMemoryStore(StringRedisTemplate redisTemplate, int expireSeconds) { this.redisTemplate = redisTemplate; this.expireSeconds = expireSeconds; } public RedisPersistentChatMemoryStore(StringRedisTemplate redisTemplate,String keyPrefix ,int expireSeconds) { this.redisTemplate = redisTemplate; this.expireSeconds = expireSeconds; this.keyPrefix = keyPrefix; } @Override public List<ChatMessage> getMessages(Object memoryId) { var key = keyPrefix + memoryId.toString(); var json = redisTemplate.opsForValue().get(key); if(Objects.isNull(json)) return List.of(); return ChatMessageDeserializer.messagesFromJson(json); } @Override public void updateMessages(Object memoryId, List<ChatMessage> list) { var key = keyPrefix + memoryId.toString(); ChatMessageSerializer.messagesToJson(list); //每次更新都更新过期时间,并且无需处理消息数量,消息数量由上层MessageWindowChatMemory或TokenWindowChatMemory 窗口处理 //并且上层会一直保留第一个系统消息 redisTemplate.opsForValue().set(key,ChatMessageSerializer.messagesToJson(list), Duration.ofSeconds(expireSeconds)); } @Override public void deleteMessages(Object memoryId) { var key = keyPrefix + memoryId.toString(); redisTemplate.expire(key,Duration.ofSeconds(0)); } }
- 初始化reidsChatMemory
this.chatMemoryStore = new RedisPersistentChatMemoryStore(redisTemplate, 60*60);
- 带聊天记忆的chat,注意sessionId使用,系统消息不会从消息窗口里删除
private ChatMemory getChatMemory(String sessionId){
return MessageWindowChatMemory.builder()
.chatMemoryStore(chatMemoryStore)
.maxMessages(maxStoreMessages)
.id(sessionId)
.build();
}
//带聊天记忆的chat,输出sessionId和ai结果
public Tuple2<String,String> chatWithMemory(AiChatRequest request){
var sessionId = request.getSessionId();
var isFirst = false;
if(StringUtils.isBlank(sessionId)) {
sessionId = UuidUtil.genUuid();
isFirst = true;
}
var chatMemory = getChatMemory(sessionId);
if(isFirst)
chatMemory.add( SystemMessage.from(DEFAULT_SYSTEM_PROMPT));
chatMemory.add(genUserMsg(request.getImgUrls(),request.getText()));
var useVlModel = CollectionUtil.isNotEmpty(request.getImgUrls());
var chatRequest = genChatRequest(chatMemory,useVlModel);
var chatResp = chatModel.chat(chatRequest);
var aiMsg = chatResp.aiMessage();
chatMemory.add(aiMsg);
return Tuple.of(sessionId, aiMsg.text());
}